package top.whsu.FileServer;

import top.whsu.messages.*;
import top.whsu.utils.*;

import java.io.*;
import java.net.*;

public class Server extends ServerSocket {

    private File baseDirectory;
    private int connectedClientCnt;
    private int connected, maxConnection;

    public Server(int portNum, String baseDirectoryPath, int maxConnection) throws FileNotFoundException, IOException {
        super(portNum);  // throws IOException
        this.baseDirectory = new File(baseDirectoryPath);
        this.connectedClientCnt = 0;
        this.maxConnection = maxConnection;
        this.connected = 0;

        // validate baseFolder
        if (!(this.baseDirectory.isDirectory())) {
            throw new FileNotFoundException(String.format("\"%s\": Base Directory does not Exist.", baseDirectoryPath));
        } else if (!(this.baseDirectory.canWrite())) {
            throw new FileNotFoundException(String.format("\"%s\": Base Directory not writable.", baseDirectoryPath));
        }
    }

    /**
     * outer interface
     */
    public void load() throws IOException {
        while (true) {
            Socket clientSocket = super.accept();  // throws
            while (this.connected>=this.maxConnection) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();  // re-interrupt the thread
                    e.printStackTrace();
                }
            }
            new Thread(new FileTransmitTask(clientSocket)).start();
        }
    }

    /**
     * using multi-threading
     */
    class FileTransmitTask implements Runnable {

        // reveived headers
        private String fileName;
        private int fileSizeInBytes;

        // logging
        private int logPercentage;
        private ObjectOutputStream logStream;

        private Socket clientSocket;
        private byte[] buffer;
        private int id;

        public FileTransmitTask(Socket clientSocket) {
            this.clientSocket = clientSocket;
            this.buffer = new byte[1024];
            this.logPercentage = 0;

            this.id = connectedClientCnt++;
        }

        public FileTransmitTask(Socket clientSocket, int bufferSize) {
            this.clientSocket = clientSocket;
            this.buffer = new byte[bufferSize];
            this.logPercentage = 0;

            this.id = connectedClientCnt++;
        }

        public void run() {
            StringBuilder errorMessageBuilder = new StringBuilder("id " + this.id);
            try {
                connected++;
                getHeader();
                InputStream fileDownloadStream = this.clientSocket.getInputStream();
                OutputStream fileWriteStream = new FileOutputStream(new File(baseDirectory, this.fileName));
                this.logStream = new ObjectOutputStream(this.clientSocket.getOutputStream());

                int readsize = 0;
                int receivedSize = 0;

                this.log(receivedSize);  // acknowledge to client

                while ((readsize = fileDownloadStream.read(buffer, 0, buffer.length)) != -1) {
                    fileWriteStream.write(buffer, 0, readsize);
                    receivedSize += readsize;
                    this.log(receivedSize);
                }

            } catch (IOException e) {
                errorMessageBuilder.append(e.getMessage());
                new IOException(errorMessageBuilder.toString()).printStackTrace();
            } catch (ClassNotFoundException e) {
                errorMessageBuilder.append(e.getMessage());
                new ClassNotFoundException(errorMessageBuilder.toString()).printStackTrace();
            } finally {
                try{
                    this.clientSocket.close();
                } catch (IOException e) { }
                connected--;
            }
        }

        private void getHeader() throws IOException, ClassNotFoundException {
            ObjectInputStream headerReceiveStream = null;
            ClientHeader header = null;
            StringBuilder errorMessageBuilder = new StringBuilder("id "+this.id);

            try {
                headerReceiveStream = new ObjectInputStream(this.clientSocket.getInputStream());
            } catch (IOException e) {
                errorMessageBuilder.append(": Header stream initialization failed.");
                throw new IOException(errorMessageBuilder.toString());
            }

            try {
                header = (ClientHeader) headerReceiveStream.readObject();
            } catch (ClassNotFoundException e) {
                errorMessageBuilder.append(": Header receiving error.");
                throw new ClassNotFoundException(errorMessageBuilder.toString());
            }

            this.fileName = header.fileName + "." + header.fileSuffix;
            this.fileSizeInBytes = header.fileSize;
        }

        private void log(int receivedSize) {

            // Display
            if (receivedSize==0) {  // acknowledge to client
                System.out.printf("id %d: Transmit started.\n", this.id);
            } else if (fileSizeInBytes==receivedSize) {
                System.out.printf("id %d: \"%s\": Successfully transferred %s.\n", this.id, this.fileName, DataSizeFormat.format(fileSizeInBytes));
            } else if (100*receivedSize/this.fileSizeInBytes!=this.logPercentage) {
                this.logPercentage = 100*receivedSize/this.fileSizeInBytes;
                System.out.printf("id %d: Transmit progress %f%% (%s of %s).\n", this.id, 100.0*receivedSize/this.fileSizeInBytes, DataSizeFormat.format(receivedSize), DataSizeFormat.format(this.fileSizeInBytes));
            }

            // Networking
            try{
                this.logStream.writeObject(new ServerLog.Builder().setReceivedSize(receivedSize).build());
                this.logStream.flush();
            } catch (IOException e) {
                StringBuilder errorMessageBuilder = new StringBuilder("id "+this.id);
                errorMessageBuilder.append(e.getMessage());
                new IOException(errorMessageBuilder.toString()).printStackTrace();
            }
        }
    }
}
